fix(rest-api): unwrap WebApplicationException from JasperException in rules include endpoint#35499
fix(rest-api): unwrap WebApplicationException from JasperException in rules include endpoint#35499dsolistorres wants to merge 2 commits intomainfrom
Conversation
|
Claude finished @dsolistorres's task in 5m 43s —— View job 🔍 dotCMS Backend ReviewTasks
[🟠 High]
// 404 is returned here — before permissions are evaluated
if (contentlet == null || !UtilMethods.isSet(contentlet.getIdentifier())) {
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND)...);
}
// Permission check comes AFTER existence check
if (!APILocator.getPermissionAPI().doesUserHavePermission(...)) {
throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN)...);
}💡 Use a permission-aware API call (e.g. [🟡 Medium]
} catch (ServletException e) {
e.printStackTrace(); // ❌ must be Logger.error(...)
}💡 Replace with [🟡 Medium]
var langIsoCode = '<%= request.getSession().getAttribute(Globals.LOCALE_KEY) %>';💡 Wrap with [🟡 Medium]
ResponseBuilder builder = Response.status(500);
return builder.build(); // no Cache-Control header on error path💡 Add Next steps
|
🔍 dotCMS Backend Review[🔴 Critical]
sw.append(e.toString()); // internal exception details
return sw.toString(); // returned as HTTP 200 by getLayout💡 After the cause-chain loop, throw a [🟠 High]
sw.append("unable to parse: <a href='" + path + "' target='debug'>" + path + "</a>");
sw.append(e.toString()); // may echo request-derived data; no HTML-encoding💡 The cleanest fix is the 🔴 Critical fix above (throw 500 instead). If the debug HTML must be kept, use [🟠 High]
// No user or permission argument — any backend session can probe
contentlet = APILocator.getContentletAPI().findContentletByIdentifierAnyLanguage(id);💡 Resolve the user from the session and verify READ permission via [🟡 Medium]
} catch (final Exception e) {
throw new WebApplicationException(
Response.status(Response.Status.BAD_REQUEST) ...
);
}💡 Catch [🟡 Medium]
final javax.servlet.ServletException wrapped =
new javax.servlet.ServletException("jsp failed", root);💡 Add Next steps
|
b3cd088 to
c8bf0d7
Compare
… rules include endpoint (#34888) Tomcat/Jasper wraps any Throwable raised inside a JSP in a ServletException before it leaves RequestDispatcher.include(), so the dedicated catch(WebApplicationException) added in #35337 never matched at runtime and /api/portlet/rules/include kept returning HTTP 200 with debug HTML instead of the expected 400/404. The generic catch in BaseRestPortlet.getJspResponse() now walks the cause chain and re-throws the WebApplicationException so JAX-RS can map the proper status. Added unit tests that simulate the real Jasper wrapping (the existing test mocked the dispatcher to throw the exception directly, bypassing the wrapper). Also in include.jsp: - Validate the id parameter against UUIDUtil.isUUID before hitting the API, so an invalid format returns 400 instead of 404. - Add DEBUG logging on every rejection branch; only log the id value once it has passed UUID validation, to avoid CWE-117 log injection from the unvalidated query parameter. Refs: #34888 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rce READ permission in rules include (#34888) BaseRestPortlet.getJspResponse(): - Replaced the legacy debug-HTML fallback with a WebApplicationException(500). Non-WebApplicationException failures (e.g. NullPointerException, IOException) now log the full stack trace at ERROR for admins and return a plain 500 with no entity, instead of HTTP 200 with internal exception details rendered as HTML. Side effect: also closes the pre-existing XSS surface where path and e.toString() were interpolated into the error markup unencoded. include.jsp: - Added explicit READ permission check via PermissionAPI.doesUserHavePermission after findContentletByIdentifierAnyLanguage. Anonymous sessions and users without READ on the contentlet now receive 403 with a generic "Access denied" body, instead of leaking existence via 200/404. BaseRestPortletTest: - Rewrote getJspResponse_returnsErrorHtmlForGenericException and getJspResponse_returnsErrorHtmlForRuntimeException as getJspResponse_throws500ForGenericException / getJspResponse_throws500ForRuntimeException; both assert WAE(500) propagates and getResponse().hasEntity() == false to lock in the no-detail-leak contract. - Replaced fully qualified javax.servlet.ServletException with the imported simple name for consistency with the rest of the file. Refs: #34888 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c8bf0d7 to
e34dcfb
Compare
Summary
Fixes #34888 — follow-up to #35337, which only partially closed the bug.
/api/portlet/rules/includewas still returning HTTP 200 withunable to parse: …debug HTML instead of the expected 400 / 404 for missing/invalid/non-existentidvalues, even after #35337 merged. Verified onmain.Root cause
When a JSP raises any
Throwable, Tomcat/Jasper wraps it in aJasperException(aServletException) before it leavesRequestDispatcher.include(). So inBaseRestPortlet.getJspResponse()the dedicatedcatch (WebApplicationException e) { throw e; }block added in #35337 never matched at runtime — the exception arrived as aServletExceptionwhose cause was the WAE. Control fell through tocatch (Exception e), which builds the legacy"unable to parse: …"HTML and returns it asResponse.ok(..., "text/html").The unit test in #35337 mocked
RequestDispatcher.include()to throwWebApplicationExceptiondirectly, bypassing the Jasper wrapping — so it passed despite the runtime bug, giving a false green.Fix
BaseRestPortlet.getJspResponse()— inside the existingcatch (Exception e)block, walk the cause chain and re-throw if any cause is aWebApplicationException. JAX-RS then maps the proper HTTP status from the response carried by the exception.BaseRestPortletTest— two new regression tests that simulate the real container behavior: aServletExceptionwhose cause is aWebApplicationException, and a deeper nested chain.include.jsp— additional QA hardening on top of the propagation fix:idagainstUUIDUtil.isUUID(id)before the API call. Now?id=not-a-valid-uuidreturns 400 instead of 404, while both 32-hex and dashed-UUID formats are accepted.Logger.debug(...)lines on each rejection branch. To avoid CWE-117 (log injection) from an unvalidated query parameter, the invalid-format branch logs the rejection without the value; the API-failure and not-found branches include the value because at that point it has already passed UUID validation.Test plan
GET /api/portlet/rules/include(noid) → 400 ✅GET /api/portlet/rules/include?id=→ 400 ✅GET /api/portlet/rules/include?id=not-a-valid-uuid→ 400 ✅GET /api/portlet/rules/include?id=00000000-0000-0000-0000-000000000000→ 404 ✅GET /api/portlet/rules/include?id=00000000000000000000000000000000→ 404 ✅GET /api/portlet/rules/include?id=<valid Site or HTML Page id>→ 200 with iframe HTML ✅./mvnw test -pl :dotcms-core -Dtest=BaseRestPortletTestpasses (4 existing + 2 new tests) ✅just dev-run) and through the Rules tab on a Site contentlet🤖 Generated with Claude Code